<?php


namespace Mowangjuanzi\Mp4;


class Mp4
{
    /**
     * 文件路径
     * @var string
     */
    protected $fp;

    /**
     * 视频时长
     * @var float
     */
    protected $duration;

    /**
     * 宽度
     * @var int
     */
    protected $width;

    /**
     * 高度
     * @var int
     */
    protected $height;

    /**
     * 读取的长度
     * @var int
     */
    protected $length;

    /**
     * 构造函数
     * @param string $path
     * @throws \Exception
     */
    public function __construct(string $path)
    {
        $this->fp = fopen($path, 'rb');

        $this->parse();
    }

    /**
     * 对文件进行解析
     * @throws \Exception
     */
    protected function parse()
    {
        // 读取 ftyp
        $ar = $this->unpackBox();

        if ($ar['boxType'] != 'ftyp') {
            throw new \Exception("该Mp4 文件暂时不支持解析");
        } else {
            fread($this->fp, $ar['totalSize'] - 8); // 抛弃。暂时不用了
            $this->length += ($ar['totalSize'] - 8);
        }

        // 解下一个包
        $ar = $this->unpackBox();

        if ($ar['boxType'] != 'moov') {
            dd("TODO no.3");
        }

        // 这是容器包，继续解
        $ar = $this->unpackBox();

        if ($ar['boxType'] != 'mvhd') {
            dd("TODO no.4");
        } else {
            $this->parseMvhdBox($ar);

            for ($i = 0; $i < 3; $i++) { // 对宽高的解析只获取三次，如果不行再去处理
                $result = $this->parseTrakBox();

                if ($result === true) {
                    break;
                }
            }
        }
    }

    /**
     * 读取 trak
     */
    protected function parseTrakBox()
    {
        $ar = $this->unpackBox(); // 读取8个字节

        if ($ar['boxType'] != 'trak') {
            dd("TODO no.6 " . $ar['boxType']);
        }

        $start = $this->length;

        // 这是容器包，继续解
        $ar2 = $this->unpackBox();

        if ($ar2['boxType'] != 'tkhd') {
            dd("TODO no.7");
        }

        $result = $this->parseTkhdBox($ar2);

        if ($result == false) {
            $ar['totalSize'] = $ar['totalSize'] - 8 - ($this->length - $start);

            $this->skipLength($ar);
        }

        return $result;
    }

    /**
     * 抛弃有效字符
     * @param array $ar
     */
    protected function skipLength(array $ar)
    {
        while ($ar['totalSize'] > 0) {
            $buf = fread($this->fp, $ar['totalSize']);
            $buf = strlen($buf);

            $ar['totalSize'] -= $buf;
            $this->length += $buf;
        }
    }

    /**
     * 解包
     * @return array
     */
    protected function unpackBox(): array
    {
        $buf = fread($this->fp, 8);
        $this->length += 8;

        $ar = unpack('NtotalSize/NboxType', $buf);
        $ar['boxType'] = pack('N', $ar['boxType']);

        if ($ar['totalSize'] == 1) {
            $ar2 = unpack('N2extSize', fread($this->fp, 8));
            $this->length += 8;

            if ($ar2['extSize1'] > 0) {
                dd("TODO no.5");
            } else {
                $ar['totalSize'] = $ar2['extSize2'] - 8;
            }
        }

        if (in_array($ar['boxType'], ['free', 'iods', 'wide', "mdat", "meta", "udta", "gnre"])) {
            $ar['totalSize'] -= 8;

            $this->skipLength($ar);

            $ar = $this->unpackBox();
        }

        return $ar;
    }

    protected function parseMvhdBox(array $box)
    {
        // 解析版本和flag
        $extra = $this->unpackFullBox();

        if ($extra['version'] != 0) {
            dd("TODO no.8");
        }

        $buf = fread($this->fp, $box['totalSize'] - 12);
        $this->length += ($box['totalSize'] - 12);

        $ar2 = unpack('Nctime/Nmtime/NtimeScale/Nduration/Nrate/nvolume/ndummy/N2dummy2/N9matrix/N3dummy3/NnextTrack',
            $buf);

        $this->duration = (isset($ar2['duration']) ? $ar2['duration'] : $ar2['duration1']) / $ar2['timeScale'];
    }

    /**
     * 主要是解析扩展部分
     * @return array
     */
    protected function unpackFullBox(): array
    {
        $buf = fread($this->fp, 4);
        $this->length += 4;

        return unpack('Cversion/C3flags', $buf);
    }

    protected function parseTkhdBox(array $box)
    {
        $extra = $this->unpackFullBox();

        if ($extra['version'] != 0) {
            dd("TODO no.9");
        }

        $buf = fread($this->fp, $box['totalSize'] - 12);
        $this->length += ($box['totalSize'] - 12);

        $ar2 = unpack('Nctime/Nmtime/NtrackId/Ndummy/Nduration/N2dummy1/nlayer/naltGroup/nvolume/ndummy2/N9matrix/Nwidth/Nheight',
            $buf);

        $ar2['volume'] = $ar2['volume'] / 256;
        if ($ar2['width'] == 0 || $ar2['height'] == 0) { // 如果获取不到则去读取下一个块
            return false; // 音轨道，去读取下一个
        }

        $this->width = $ar2['width'] / 65536;
        $this->height = $ar2['height'] / 65536;

        return true;
    }

    /**
     * 获取宽度
     * @return int
     */
    public function getWidth(): int
    {
        return $this->width;
    }

    /**
     * 获取高度
     * @return int
     */
    public function getHeight(): int
    {
        return $this->height;
    }

    /**
     * 获取时长
     * @return int
     */
    public function getDuration(): int
    {
        return round($this->duration);
    }

    public function getLength(): int
    {
        return $this->length;
    }

    /**
     * 析构函数
     */
    public function __destruct()
    {
        unset($this->fp);
    }
}
